"""
My own attempt at a form library for Python.

Actors: Form, Fields, Views.

A field has a view

A form has a list of fields

A field has a render() method.

A view has a render() method.

"""

import formencode
import copy
from mako.template import Template

class Header(object):

    def __init__(self, form): 
        self.form = form

    def render(self):
        template = Template("""<form ${_id} ${name} ${action} ${method} ${enctype}>""", output_encoding="utf-8")
        id = "id = \"%s\"" % self.form.id
        name = "name = \"%s\"" % self.form.name
        action = "action = \"%s\"" % self.form.action
        method = "method = \"%s\"" % self.form.method
        enctype = "enctype = \"%s\"" % self.form.enctype
        return template.render(_id=id, name=name, action = action, method = method, enctype = enctype)

class Footer(object):
    def render(self):
        template = Template("""</form>""",output_encoding="utf-8")
        return template.render()
       


class Form(object):
    """
    An abstraction to simplify HTML form definition, rendering, population, validation and export.
    """
    def __init__(self): 
        """
        Typically you create your form as a subclass of this class, setting values for action, method, enctype, fields.
        """
        self.id = self.__class__.id #: the url where the form is submitted
        self.name = self.__class__.name #: the url where the form is submitted
        self.action = self.__class__.action #: the url where the form is submitted
        self.method = self.__class__.method #: get or post 
        self.enctype = self.__class__.enctype #: multipart/form-data if file upload
        self.fieldnames = self._fieldnames() #: dictionary of field objects
        self.validator = self._makeValidator()
        self.header = Header(self) # mock fields for the start and end 
        self.footer = Footer() # of the form.
    def __getattr__(self,name):
        if name in self.fieldnames.keys():
            return self.fieldnames[name]
        return self.__dict__[name]
    def __begin(self):
        enctype = action = method = None
        if hasattr(self.__class__, "enctype"): enctype = self.__class__.enctype
        if hasattr(self.__class__, "action"): action = self.action
        if hasattr(self.__class__, "method"): method = self.__class__.method
        result = ["<form"]
        if action: result.append('action="%s"' % self.action) 
        if method: result.append('method="%s"' % method) 
        if enctype: result.append('enctype="%s"' % enctype) 
        result.append(" >")
        return " ".join(result)
    def _fieldnames(self):
        fieldnames = {}
        for f in self.__class__.fields:
            fieldnames[f.name]=copy.deepcopy(f)        
        #raise ValueError, fieldnames
        return fieldnames
    def _makeValidator(self):
        # assemble validation fields from form class        
        class V(formencode.Schema): pass
        for f in self.fieldnames.values():
            if f.validator:
                V.fields[f.name] = f.validator
        #raise ValueError, V()
        return V()
    def populate(self, mapping, validate=True):
        """
        Reads the user-supplied values into the Form object.
        Iterates through each inputtable field in the form and calls the field's
        populate() method.

        The input mapping is expected to be in typical HTML form layout. 
        For example, if a field has no input, it is omitted from the dictionary,
 
        If the field name is not present in the mapping, then the populate
        method is called with a parameter of 'None'.

        NOTE!!!! if mapping is an ordinary dictionary, then values are expected to be lists.
    
        @param mapping: a mapping keyed by field name.
        @param validate: a boolean to indicate whether or not to automatically perform validation.
        @return: if validate is True then returns a boolean, otherwise returns None.
        """
        for f in self.fieldnames.values():
            name = f.name 
            input = None # used when the field name is absent from the inputDict.
            if hasattr(f, "input") :  # a hack to exclude header, footer, submit buttons etc.
                if name in mapping:
                # mod_python.FieldStorage, cgi.FieldStorage and dicts,oh my!
                    try: # FieldStorage from mod_python or cgi modules
                        input = []
                        items = mapping.getlist(name)
                        for i in items:
                            try:input.append(i.value) #mod_python
                            except: input.append(i)   #cgi
                    except: # fall back to ordinary dictionary
                        input = mapping[name] # expecting a list !
                    f.populate(input)
        if validate == True: return self.isValid()
    def isValid(self):
        """
        @return: True or False
        """
        data={}
        for f in self.fieldnames.values():
            if f.validator:data[f.name] = f.input
        try: 
            self.validator.to_python(data) 
        except formencode.Invalid, e:
            for k in e.error_dict.keys():
                self.fieldnames[k].error = e.error_dict[k].message # could be throwing away information here
            return False
        return True
    def render(self, *ids):
        """
        @param ids: the names of the fields to be rendered. If no ids are supplied then the whole form is rendered.
        @return:  a string representing the form markup.
        """
        # get the order of the fields by picking up the names from
        # the fields attribute
        names = [x.name for x in self.fields]
        ids = ids
        if not len(ids): 
            result = []
            result.append(self.header.render())
            for n in names:
                result.append(self.fieldnames[n].render())
            result.append(self.footer.render())
            return "\n".join(result)
        else:
            result = []
            for i in ids:
                result.append(self.fieldnames[i].render()) 
            return "\n".join(result )
    def export(self):
        """
        @return: a dictionary of name:input pairs for each field in fieldnames.
        """
        result = {}
        for f in self.fieldnames.values():
            result[f.name] = f.input
        return result

#######################################################    
######### FIELDS ######################################    
#######################################################    

class Field(object):
    """
    This is the key class within the formgold library. 
    """
    def __init__(self, id=None, name = None, label = None, validator = None, view = None, input=None ):
        """
        The fields attribute of your Form sublcass is usually a list of Field objects.
        Usually you will add a subclass rather than an instance of this class.

        Note, for now the id attribute is equal to the id name.
        @param id: the html id of the field.
        @param name: the name of the field.
        @param label: human-readable description.
        @param validator: a formencode validator.
        @param view: an instance of a formgold.View class.
        """
        self.id = name#: the name of the field.
        self.name = name #: the name of the field.
        self.label = label #: a human readable description.
        self.validator = validator #: from formencode.
        self.view = view #: an instance of a view.
        self.input = input #: the string,list of strings, or file descriptor as input by submitter.
        self.error = None #: populated on formencode validation.
        if not self.name: raise ValueError, "Name cannot be None"
        if not self.view: self.view = FieldXhtmlView() # and Field object must have _defaultView attribute
        if not self.label: self.label = """Label for %s field""" % (self.name,)
        self.view.setField(self)
    def render(self):
        """
        Renders the field's markup.
        Usually simply calls the correspondingly named method of the View class.
        @return: a string of markup representing the field.
        """
        return self.view.render()

    def renderJS(self):
        """
        If any javascript is required to enable the field, it would be rendered here. 

        Usually simply calls the correspondingly named method of the View class.
        @return: a string of javascript code.
        """
        return self.view.renderJS()

class TextArea(Field):
    def __init__(self, name = None, label = None, validator = None, view = None, input = None):
        self.input = input 
        if not view: view = TextAreaFieldXhtmlView()
        Field.__init__(self, name = name, label = label, validator = validator, view = view, input=input )

    def populate(self, v):
        if type(v)==type([]):
            self.input = v[0]
        else: self.input = v

class InputField(Field):
    def __init__(self, name = None, label = None, validator = None, view = None, input = None):
        self.input = input 
        if not view: view = InputFieldXhtmlView()
        Field.__init__(self, name = name, label = label, validator = validator, view = view , input=input)
    def populate(self, raw_input):
        if type(raw_input)==type([]):
            self.input = raw_input[0]
        else: self.input = raw_input

class ChoiceField(InputField):
    """
    Either a radio button or a checkbox
    """
    def __init__(self, name = None, label=None, checked=None, validator = None, view = None, input=None):
        InputField.__init__(self, name=name,label=label, validator = validator, view = view, input=input) 
        self.checked = checked

    def populate(self, v):
        temp = None
        if type(v)==type([]): temp = v[0]
        else: temp = v 
        if temp == self.input: self.checked = True
        else: self.checked = False
            

class TextField(InputField):
    type = "text"
class SubmitField(InputField):
    type = "submit"   
class PasswordField(InputField):
    type = "password"
class FileField(InputField):
    type = "file"
class ResetField(InputField):
    type = "reset"
class ButtonField(InputField): 
    type = "button"
class HiddenField(InputField): 
    type = "hidden"
    def __init__(self, name = None, label=None, input=None, validator = None, view = None):
        if view == None: view = HiddenFieldXhtmlView() 
        InputField.__init__(self, name = name, label = label, validator = validator, view = view, input=input)
    
class ImageField(InputField):
    type = "image"
    src = None
class Checkbox(ChoiceField):
    type = "checkbox"
    
    def __init__(self, name = None, label=None, input=None, checked=None, validator = None, view = None):
        if view == None: view = CheckboxFieldXhtmlView() 
        ChoiceField.__init__(self, name = name, label = label, validator = validator, view = view, input=input, checked=checked)

class RadioButton(ChoiceField):
    type = "radio"
 
class Option(object):
    def __init__(self, display=None, value=None):
        self.display= display
        self.value= value
        self.view = OptionFieldXhtmlView()
        self.view.setField(self)
    def render(self):
        return self.view.render()
    def renderJS(self):
        return self.view.renderJS()

##### Logical Fields (?) #############
class Boolean(Field):
    """
    Its export value is either true or false
    """
    type = "checkbox"
    def __init__(self, name = None, label = None, validator = None, view = None, input = None):
        if input not in [True, False]: raise ValueError, "input must be True or False, not >%s<" % str(input)
        self.input = input
        if not view:
            view = BooleanFieldXhtmlView()
        Field.__init__(self, name = name, label = label, validator = validator, view = view ,input = self.input)

    def populate(self, v):
        self.input = False
        temp = None
        if type(v)==type([]): temp = v[0]
        else: temp = v
        if temp in [True, 1, "1","yes", "YES", "Yes", "True", "true", "TRUE"]:
            self.input = True
        
         
            
 


class Choice(Field):
    """
    A Choice field returns either an empty list, a list of exactly one item,
    or a list of more than one item.
    """
    def __init__(self, name = None, label = None, validator = None, view = None, multi = True, options = None, input = None , display="compact"):
        """
        @param multi: boolean, one value or more?
        @param options: a list of Option objects from which the user chooses.
        @param input: a string or list of Option object values defining the initial state of the field.
        @param display: either 'normal' or 'compact'.
        """
        self.multi = multi #: multiple values are allowed?
        self.display = display
        self.options = options
        self.input = input 
        if self.input == None and self.multi : self.input = [] 
        if not self.options: raise ValueError, "A choice field must have some options"
        if not view:
            if self.display == "compact":
                view = SelectFieldXhtmlView()
            else: 
                if self.multi == False:
                    view = RadioSelectFieldXhtmlView() # new class
                else:
                    view = CheckboxSelectFieldXhtmlView() # new class
        Field.__init__(self, name = name, label = label, validator = validator, view = view ,input = self.input)

    def populate(self,v):
        """
        If multi=False, a list should be forbidden. ie self.input is None
        or some other single value.  ie only return lists when multi is True.
      
        Accepts the input from the submitted form and tries to make sense of it.
     
        If multi = True, v is None, sets self.input = []. 
        If v is a list, self.input=v, if v is an item, then 
        self.input = [v].  

        @param v: a value from the submitted form, or None if field omitted from form.
        """
        if self.multi:
            if v == None: self.input = [] 
            elif type(v) == type([]): self.input = v
            else: self.input = [v]
        else:
            if v == None: self.input = None
            elif type(v) == type([]): self.input = v[0] 
            else: self.input = v

##########################################
########### VIEWS ########################
##########################################

class FieldXhtmlView(object):

    def setField(self,field):
        self.field = field

    def render(self):
        return u"""<div><p>%s</p><p>Default view for %s field</p></div>""" % (self.field.label,self.field.name)

    def renderJS(self):
        return ""

class OptionFieldXhtmlView(FieldXhtmlView) :

    def __init__(self):
        self.selected = None # initialised by direct assignment elsewhere.

    def render(self):
        template = Template("""<option ${selected} ${value}>${display}</option>""", output_encoding="utf-8")
        selected = ""
        if self.selected: selected = "selected = \"yes\""
        value = "value = \"%s\"" % self.field.value
        return template.render_unicode(selected = selected, value = value, display = self.field.display)
    
class SelectFieldXhtmlView(FieldXhtmlView) :
    """
    if input, then use input, else use selected
    """

    def render(self):
        template = Template("""
% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
<label for="${field.name}">${label}</label>
<select ${name} ${multi}>${options}</select>
</div>
""", output_encoding="utf-8")
        workingSel = self.field.input
        if not self.field.multi: workingSel = [self.field.input]
        #if not workingSel: raise ValueError, "".join([self.field.name, str(workingSel)])
       
        # this assumes that the input comes in as a list!
        # at the point of populating the form, the input needs to be 
        # inspected to see if it is for a Choice field, and if yes turn
        # it into a list.
        options = []
        for o in self.field.options:
            selected = False
            if o.value in workingSel: selected = True
            o.view.selected = selected
            options.append(o.view.render())
        options = "".join(options)
        multi = ""
        if self.field.multi: multi = "multiple = \"yes\""
        name = "name = \"%s\"" % self.field.name
       
        return template.render_unicode( field = self.field, name = name, multi = multi, label = self.field.label, options = options )

class RadioSelectFieldXhtmlView(FieldXhtmlView):

    def render(self):
        template = Template("""
% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif

<fieldset id="${field.id}">
% if field.label:
<legend>${field.label}</legend>
% endif
<%
workingsel = field.input
if not field.multi: workingsel = [field.input]
%>
% for o in field.options:
    <% id = "_".join([field.name,o.value]) %>
    % if o.value in workingsel:
        <input id="${id}" type="radio" name="${field.name}" value="${o.value}" checked="checked" /> 
    % else:
        <input id="${id}" type="radio" name="${field.name}" value="${o.value}" /> 
    % endif
    <label>${o.display}</label>
% endfor
</fieldset>
</div>
""",output_encoding="utf-8")
        return template.render_unicode( field = self.field )

class CheckboxSelectFieldXhtmlView(FieldXhtmlView):

    def render(self):
        template = Template("""
% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
<fieldset id="${field.id}">
% if field.label:
<legend>${field.label}</legend>
% endif

<%
workingsel = field.input
if not field.multi : workingsel = [field.input]
%>

% for o in field.options:
    <% id = "_".join([field.name,o.value]) %>
    % if o.value in workingsel:
        <input id="${id}" type="checkbox" name="${field.name}" value="${o.value}" checked="checked" />
    % else:
        <input id="${id}" type="checkbox" name="${field.name}" value="${o.value}" />
    % endif
    <label>${o.display}</label>
% endfor
</fieldset>
</div>
""", output_encoding="utf-8")
        try:return template.render_unicode( field = self.field )
        except: return "<h1>%s</h1>" % self.field.name


class TextAreaFieldXhtmlView(FieldXhtmlView):
    template = Template("""
% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
% if field.label:
<label for="${field.name}">${field.label}</label>
% endif
<textarea ${name}>${content}</textarea>
</div>
""", output_encoding='utf-8')

    def render(self):
        name = "name = \"%s\"" % self.field.name
        #if there as input, use that, if not, use content
        content = None
        if hasattr(self.field, "input"): 
            content = self.field.input
        return self.template.render_unicode(name=name, content = content, field = self.field)
    

class InputFieldXhtmlView(FieldXhtmlView):
    """
    The template needs to be customised for image, button, reset,
    checkbox, radio etc types.
    """

    def render(self):
        template = Template("""
<%
showValue = field.input
if showValue is None: showValue = ""
if field.input is not None: showValue = field.input
%>

% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
% if field.label:
<label for="${field.name}">${field.label}</label>
% endif
<input type = "${field.type}" name = "${field.name}" value = "${showValue}"/>
</div>
""",output_encoding="utf-8")
        return template.render_unicode(field = self.field)

class HiddenFieldXhtmlView(FieldXhtmlView):
    """
    View for hidden field. (No labels)
    """

    def render(self):
        template = Template("""
<%
showValue = field.input
if showValue is None: showValue = ""
if field.input is not None: showValue = field.input
%>

% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
<input type = "${field.type}" name = "${field.name}" value = "${showValue}"/>
</div>
""", output_encoding="utf-8")
        return template.render_unicode(field = self.field)



class CheckboxFieldXhtmlView(FieldXhtmlView):

    def render(self):
        template = Template("""
<%
checked = ""
if field.checked: checked = "checked='checked'"
%>

% if field.label:
<p>${field.label}</p>
% endif
% if field.error:
<p>${field.error}</p>
% endif
<input ${checked} type = "${field.type}" name = "${field.name}" value = "${field.input}"/>
""", output_encoding="utf-8")
        return template.render_unicode(field = self.field)

class BooleanFieldXhtmlView(FieldXhtmlView):

    def render(self):
        template = Template("""


<%
checked = ""
if field.input: checked = "checked='checked'"
%>


% if field.error:
<div class="error">
<p>${field.error}</p>
% else:
<div>
% endif
% if field.label:
<label for="${field.name}">${field.label}</label>
% endif
<input ${checked} type = "${field.type}" name = "${field.name}" value = "True"/>
</div>
""", output_encoding="utf-8")
        return template.render_unicode(field = self.field)


